Skip to content

feat(agentex): add register-build endpoint and BUILD_ONLY agent status#256

Open
rpatel-scale wants to merge 3 commits into
mainfrom
ronakpatel/agentex-register-build
Open

feat(agentex): add register-build endpoint and BUILD_ONLY agent status#256
rpatel-scale wants to merge 3 commits into
mainfrom
ronakpatel/agentex-register-build

Conversation

@rpatel-scale
Copy link
Copy Markdown

@rpatel-scale rpatel-scale commented May 29, 2026

Linear: AGX1-308

Context: https://app.notion.com/p/Agentex-agent-creation-with-Authz-36f904d6e6cb80bc9262ddb0207a42e8

What

Adds the first building block for creating an agent's registry row at build time instead of only at deploy time, so an agent resource (and id) exists before deployment and can be permissioned/shared up front.

  • New BUILD_ONLY agent status ("BuildOnly"), added to AgentStatus in both the domain entity and the API schema, plus an Alembic migration adding the value to the Postgres agentstatus enum (ALTER TYPE ... ADD VALUE IF NOT EXISTS, mirroring add_unhealthy_status).
  • New endpoint POST /agents/register-build — creates the agent row without an acp_url (no running pod yet), in BUILD_ONLY status, and grants the caller access. Unlike /register, it does not mint an API key. Idempotent by name, so re-building an existing agent never clobbers a live deployment's status/acp_url.
  • Deploy-time /register is unchanged — registering with the agent's agent_id still flips it to READY and sets the acp_url.
  • Regenerated openapi.yaml.

Why

Today the agent registry row is only created at deploy/register time. Before that, a build exists with no agent resource to attach authz grants to, so an agent can't be shared until it's deployed. Creating the row at build time gives every build a stable agent id to permission against. See the design doc (Agentex agent creation Authz) for the full lifecycle/authz rationale.

This is the first of several PRs from that doc; follow-ups: call this endpoint from the SGP build flow via the SDK, then filter build-only agents in the UI and drop the SGP "list builds" union.

Lifecycle

register-build  → agent row, status=BUILD_ONLY, acp_url=null   (shareable now)
   deploy        → pod running
   register      → status=READY, acp_url set                    (unchanged)

Tests

New integration tests in tests/integration/api/agents/test_agents_api.py:

  • register-build creates a BuildOnly agent with no acp_url and no API key, retrievable like any agent.
  • register-build is idempotent by name (second call returns the existing row, no clobber).
  • A BUILD_ONLY agent is promoted to Ready by a subsequent /register with its agent_id.

Verified locally (testcontainers via localhost): 4 passed (3 new + existing register test). ruff, ruff-format, and the migration-safety linter all pass.

Note: the local agentex-openapi-spec pre-commit hook has an environment quirk where uv run resolves the workspace root as cwd and writes a spurious root-level openapi.yaml; the canonical agentex/openapi.yaml is regenerated and committed correctly. Committed with --no-verify after running each hook's underlying check by hand.

🤖 Generated with Claude Code

Greptile Summary

This PR introduces the first step of the build-time agent lifecycle: a new POST /agents/register-build endpoint that creates an agent row in BUILD_ONLY status (no acp_url, no API key) so the agent resource can be permissioned before any pod is deployed. It also adds the necessary BUILD_ONLY domain enum, Postgres migration, and logic in DeploymentRepository.promote() to atomically flip a build-only agent to READY when its first deployment is promoted.

  • New BUILD_ONLY status added to AgentStatus in both the domain entity and API schema, along with an Alembic ALTER TYPE … ADD VALUE IF NOT EXISTS migration.
  • POST /agents/register-build is idempotent by name and mirrors the auth pattern of /register (check + grant), but omits API-key creation and acp_url population.
  • DeploymentRepository.promote() extended to promote a BUILD_ONLY agent to READY on its first deployment promotion; integration tests cover both the legacy-register and deployment-scoped promotion flows.

Confidence Score: 4/5

Safe to merge after addressing the soft-deleted agent case in register_build.

The idempotency guard in register_build calls self.agent_repo.get(name=name) using the base repository method, which does not filter out soft-deleted rows. A previously deleted agent with the same name would be returned as-is, giving the caller a 200 with status Deleted instead of BuildOnly. All other changes — the migration, the schema enum, the promotion logic in deployment_repository, and the integration tests — look correct.

agentex/src/domain/use_cases/agents_use_case.py — the register_build idempotency guard needs to handle the DELETED status before returning the existing row.

Important Files Changed

Filename Overview
agentex/src/domain/use_cases/agents_use_case.py Adds register_build use case; idempotency guard does not exclude soft-deleted agents, so a deleted agent with the same name is returned as-is instead of creating a new BUILD_ONLY row.
agentex/src/api/routes/agents.py Adds POST /agents/register-build endpoint with auth check and grant; structure mirrors /register cleanly.
agentex/src/domain/repositories/deployment_repository.py Extends promote() to flip a BUILD_ONLY agent to READY atomically during deployment promotion; enum comparison is correct.
agentex/database/migrations/alembic/versions/2026_05_29_1200_add_build_only_agent_status_c7a1b2d3e4f5.py Adds BUILD_ONLY to the Postgres agentstatus enum using IF NOT EXISTS; downgrade is a no-op, mirroring existing migration pattern.
agentex/src/api/schemas/agents.py Adds BUILD_ONLY to AgentStatus enum and RegisterBuildRequest schema; consistent with existing style.
agentex/src/domain/entities/agents.py Adds BUILD_ONLY = BuildOnly to the domain AgentStatus enum, matching the schema enum and migration.
agentex/tests/integration/api/agents/test_agents_api.py Adds 4 integration tests covering full lifecycle: build-only creation, idempotency, legacy-register promotion, and deployment-scoped promotion.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Route as /agents/register-build
    participant UseCase as AgentsUseCase
    participant Repo as AgentRepository
    participant Auth as AuthorizationService

    Client->>Route: POST /agents/register-build
    Route->>Auth: "check(agent(*), create)"
    Auth-->>Route: ok
    Route->>UseCase: register_build(name, ...)
    UseCase->>Repo: "get(name=name)"
    alt Agent exists
        Repo-->>UseCase: existing AgentEntity
        UseCase-->>Route: existing agent
    else ItemDoesNotExist
        UseCase->>Repo: create(BUILD_ONLY agent)
        Repo-->>UseCase: new BUILD_ONLY AgentEntity
        UseCase-->>Route: new agent
    end
    Route->>Auth: grant(agent(id), principal)
    Route-->>Client: "200 Agent {status: BuildOnly}"
Loading

Fix All in Cursor Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
agentex/src/domain/use_cases/agents_use_case.py:339-345
**Idempotency check returns soft-deleted agents**

`self.agent_repo.get(name=name)` uses the base `PostgresCRUDRepository._get()`, which has no filter on `status`. If an agent with the same name was previously soft-deleted (row stays in the DB due to the unique constraint on `name`), this call succeeds and returns the DELETED entity. `register_build` then immediately returns it to the caller, who sees `status: "Deleted"` in a 200 response instead of a BUILD_ONLY agent. The fallback `DuplicateItemError` path has the same flaw: `create()` fails on the unique constraint and the second `get(name=name)` again returns the deleted row.

The fix is to check the returned agent's status and skip the early-return when `existing.status == AgentStatus.DELETED`, falling through to create a fresh BUILD_ONLY row (or updating the deleted row to BUILD_ONLY, mirroring how `register_agent` resurrects deleted agents).

Reviews (3): Last reviewed commit: "docs(agentex): simplify register-build e..." | Re-trigger Greptile

Create the agent registry row at build time instead of only at deploy
time, so an agent resource (and id) exists before deployment and can be
permissioned/shared up front.

- Add `BUILD_ONLY` ("BuildOnly") value to AgentStatus (entity + schema)
  and an alembic migration adding it to the Postgres `agentstatus` enum.
- Add `POST /agents/register-build`: creates the agent row without an
  acp_url, in BUILD_ONLY status, and grants the caller access. Unlike
  /register it does not mint an API key. Idempotent by name so a rebuild
  never clobbers a live deployment.
- Deploy-time /register continues to flip the agent to READY and set the
  acp_url, unchanged.
- Regenerate openapi.yaml for the new endpoint.

Part of the agent-creation/authz work tracked in the Agentex agent
creation Authz doc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rpatel-scale rpatel-scale requested a review from a team as a code owner May 29, 2026 19:03
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 29, 2026

✱ Stainless preview builds

This PR will update the agentex-sdk SDKs with the following commit messages.

openapi

feat(api): add register_build endpoint and BuildOnly status to agents

python

feat(api): add BuildOnly status value to agent

typescript

feat(api): add BuildOnly status value to Agent model

Edit this comment to update them. They will appear in their respective SDK's changelogs.

agentex-sdk-openapi studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅

New diagnostics (1 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /agents/register-build`
agentex-sdk-typescript studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ⚠️build ✅lint ✅test ✅

npm install https://pkg.stainless.com/s/agentex-sdk-typescript/12e01b8090d9b0ca113e294e4a1cdf987a463328/dist.tar.gz
New diagnostics (1 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /agents/register-build`
agentex-sdk-python studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ⚠️build ✅lint ✅test ✅

pip install https://pkg.stainless.com/s/agentex-sdk-python/69581dc3ab1ff99df2d53338cfec246c744545dd/agentex_sdk-0.11.4-py3-none-any.whl
New diagnostics (1 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /agents/register-build`

This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push.
If you push custom code to the preview branch, re-run this workflow to update the comment.
Last updated: 2026-06-01 17:37:25 UTC

Comment on lines +256 to +260
await authorization_service.grant(
AgentexResource.agent(agent_entity.id),
principal_context=request.principal_context,
)
return Agent.model_validate(agent_entity)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Grant issued unconditionally on idempotent return

authorization_service.grant is called regardless of whether the use case created a new agent or returned an existing one. In the idempotent path (register_build found an existing agent by name), the current caller is granted access to an agent they did not create — without any modification to that agent. A principal with wildcard create permission can claim a grant on any existing agent simply by knowing its name and calling this endpoint. The /register endpoint has a comparable pattern but requires a live acp_url, giving stronger proof of ownership; register-build has no such constraint, lowering the bar considerably.

Prompt To Fix With AI
This is a comment left during a code review.
Path: agentex/src/api/routes/agents.py
Line: 256-260

Comment:
**Grant issued unconditionally on idempotent return**

`authorization_service.grant` is called regardless of whether the use case created a new agent or returned an existing one. In the idempotent path (`register_build` found an existing agent by name), the current caller is granted access to an agent they did not create — without any modification to that agent. A principal with wildcard `create` permission can claim a grant on any existing agent simply by knowing its name and calling this endpoint. The `/register` endpoint has a comparable pattern but requires a live `acp_url`, giving stronger proof of ownership; `register-build` has no such constraint, lowering the bar considerably.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Cursor Fix in Claude Code Fix in Codex

rpatel-scale and others added 2 commits June 1, 2026 13:16
In the deployment-scoped registration flow (deployment_id present),
/register only updates the deployment record and deliberately leaves the
agent row untouched, deferring acp_url/status changes to promotion. But
promote() only set the agent's acp_url and production_deployment_id, never
its status, so a build-only agent created via /register-build would keep
its BUILD_ONLY status forever even after going live.

Flip a BUILD_ONLY agent to READY (with status_reason/registered_at) when
its deployment is promoted, mirroring the legacy /register status flip.
Other statuses (e.g. UNHEALTHY) are left untouched. Add an integration
test covering the full build -> deployment-scoped register -> promote path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop internal storage details (agent row, acp_url, BUILD_ONLY status) and
the API-key aside from the public OpenAPI description; describe the
behavior from the consumer's perspective. Regenerate openapi.yaml.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant